﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NetFrameworkExtensions.ComponentModel;
using System.ComponentModel;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using NetFrameworkExtensions.Threading;

namespace NetFrameworkExtensions.Test.ComponentModel
{
  [TestClass]
  public class NotifyObjectTests
  {
    static NotifyObjectTests()
    {
      GlobalSynchronizationContext.SetCurrent(new SyncSynchronizationContext());
    }

    public NotifyObjectTests()
    {
    }

    private TestContext testContextInstance;

    public TestContext TestContext
    {
      get
      {
        return testContextInstance;
      }
      set
      {
        testContextInstance = value;
      }
    }

    private void AssertObjectWatcherResults(NotifyObjectWatcher watcher, string[] changedProperties, string[] changedChainProperties = null)
    {
      if (changedChainProperties == null)
        changedChainProperties = changedProperties;
      Assert.AreEqual(changedProperties.Length, watcher.ChangedProperties.Count, "watcher.ChangedProperties.Count == " + changedProperties.Length);
      Assert.AreEqual(changedChainProperties.Length, watcher.ChangedChainProperties.Count, "watcher.ChangedChainProperties.Count == " + changedChainProperties.Length);
      for (int i = 0; i < changedProperties.Length; i++)
      {
        Assert.AreEqual(changedProperties[i], watcher.ChangedProperties[i].PropertyName);
      }
      for (int i = 0; i < changedChainProperties.Length; i++)
      {
        Assert.AreEqual(changedChainProperties[i], watcher.ChangedChainProperties[i].Steps.ElementAt(0).PropertyArgs.PropertyName);
      }
    }

    [TestMethod]
    public void FirePropertyWithLiteral_Test()
    {
      var obj = new SimpleNotifyObject();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.FirePropertyWithLiteral = "Hello world";

        AssertObjectWatcherResults(watcher, new string[] { "FirePropertyWithLiteral" });
      }
    }

    [TestMethod]
    public void FirePropertyWithEventArgs_Test()
    {
      var obj = new SimpleNotifyObject();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.FirePropertyWithEventArgs = "Hello world";

        AssertObjectWatcherResults(watcher, new string[] { "FirePropertyWithEventArgs" });
      }
    }

    [TestMethod]
    public void FirePropertyWithLambda_Test()
    {
      var obj = new SimpleNotifyObject();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.FirePropertyWithLambda = "Hello world";

        AssertObjectWatcherResults(watcher, new string[] { "FirePropertyWithLambda" });
      }
    }

    [TestMethod]
    public void RegisterSimpleStringDependent_Test()
    {
      var obj = new NotifyObjectWithSimpleStringDependencies();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.StringProperty1 = "Hello";
        obj.StringProperty2 = "World";

        Assert.AreEqual("Hello World", obj.StringDependentProperty);

        AssertObjectWatcherResults(watcher, new string[] { "StringProperty1", "StringDependentProperty", "StringProperty2", "StringDependentProperty" }, new string[] { "StringProperty1", "StringProperty2" });
      }
    }

    [TestMethod]
    public void RegisterSimpleMixedDependent_Test()
    {
      var obj = new NotifyObjectWithSimpleMixedDependencies();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.IntProperty1 = 15;
        obj.DateTimeProperty2 = new DateTime(2010, 5, 24);

        Assert.AreEqual("15 20100524", obj.StringDependentProperty);

        AssertObjectWatcherResults(watcher, new string[] { "IntProperty1", "StringDependentProperty", "DateTimeProperty2", "StringDependentProperty" }, new string[] { "IntProperty1", "DateTimeProperty2" });
      }
    }

    [TestMethod]
    public void RegisterDoubleDependent_Test()
    {
      var obj = new NotifyObjectWithDoubleDependencies();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.StringProperty1 = "Hello";
        obj.StringProperty2 = "World";

        Assert.AreEqual("Hello World", obj.StringDependentProperty1);
        Assert.AreEqual("Hello World - World", obj.StringDependentProperty2);

        AssertObjectWatcherResults(watcher, new string[] { "StringProperty1", "StringDependentProperty1", "StringDependentProperty2", "StringProperty2", "StringDependentProperty1", "StringDependentProperty2" }, new string[] { "StringProperty1", "StringProperty2" });
      }
    }

    [TestMethod]
    public void RegisterCyclicDependent_Test()
    {
      var obj = new NotifyObjectWithCyclicDependencies();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.StringProperty1 = "Hello";
        obj.StringProperty2 = "World";

        Assert.AreEqual("Hello", obj.StringProperty1);
        Assert.AreEqual("World", obj.StringProperty2);
        Assert.AreEqual(null, obj.StringProperty3);

        AssertObjectWatcherResults(watcher, new string[] { "StringProperty1", "StringProperty2", "StringProperty3", "StringProperty2", "StringProperty3", "StringProperty1" }, new string[] { "StringProperty1", "StringProperty2" });
      }
    }

    [TestMethod]
    public void InnerFiring_Test()
    {
      var obj = new NotifyObjectWithInnerFiring();
      using (var watcher = new NotifyObjectWatcher(obj))
      {
        obj.StringProperty1 = "Hello";
        obj.StringProperty2 = "World";

        Assert.AreEqual("Hello", obj.StringProperty1);
        Assert.AreEqual("World", obj.StringProperty2);

        AssertObjectWatcherResults(watcher, new string[] { "StringProperty1", "StringProperty2", "StringProperty2" }, new string[] { "StringProperty1", "StringProperty2" });
      }
    }

    [TestMethod]
    public void SerializeAndDeserialize_Test()
    {
      var obj = new NotifyObjectWithSimpleStringDependencies();
      NotifyObjectWithSimpleStringDependencies obj2;
      obj.StringProperty1 = "StringProperty1";
      obj.StringProperty2 = "StringProperty2";

      var formatter = new BinaryFormatter();

      byte[] binary;

      using (var stream = new MemoryStream())
      {
        formatter.Serialize(stream, obj);
        binary = stream.ToArray();
      }

      using (var stream = new MemoryStream(binary))
      {
        obj2 = (NotifyObjectWithSimpleStringDependencies)formatter.Deserialize(stream);
      }

      Assert.AreEqual(obj.StringProperty1, obj2.StringProperty1);
      Assert.AreEqual(obj.StringProperty2, obj2.StringProperty2);
      Assert.AreEqual(obj.StringDependentProperty, obj2.StringDependentProperty);

      bool firedStringProperty1 = false;
      bool firedStringProperty2 = false;
      bool firedDependentProperty = false;
      obj2.PropertyChanged += (s, e) => 
      { 
        if (e.PropertyName == "StringProperty1") firedStringProperty1 = true;
        if (e.PropertyName == "StringProperty2") firedStringProperty2 = true;
        if (e.PropertyName == "StringDependentProperty") firedDependentProperty = true; 
      };
      obj2.StringProperty1 = "New Value";
      Assert.IsTrue(firedStringProperty1);
      Assert.IsFalse(firedStringProperty2);
      Assert.IsTrue(firedDependentProperty);
    }
  }

  public class NotifyObjectWatcher : IDisposable
  {
    public NotifyObjectWatcher(NotifyObject target)
    {
      if (target == null) throw new ArgumentNullException("target");
      Target = target;
      ChangedProperties = new List<PropertyChangedEventArgs>();
      ChangedChainProperties = new List<PropertyChainChangedEventArgs>();
      WireTarget();
    }

    public NotifyObject Target { get; private set; }

    public List<PropertyChangedEventArgs> ChangedProperties { get; private set; }

    public List<PropertyChainChangedEventArgs> ChangedChainProperties { get; private set; }

    public void Dispose()
    {
      if (Target != null)
      {
        UnWireTarget();
        Target = null;
      }
    }

    private void WireTarget()
    {
      Target.PropertyChanged += new PropertyChangedEventHandler(Target_PropertyChanged);
      Target.PropertyChainChanged += new PropertyChainChangedEventHandler(Target_PropertyChainChanged);
    }

    private void UnWireTarget()
    {
      Target.PropertyChanged -= new PropertyChangedEventHandler(Target_PropertyChanged);
      Target.PropertyChainChanged -= new PropertyChainChangedEventHandler(Target_PropertyChainChanged);
    }

    void Target_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      ChangedProperties.Add(e);
    }

    void Target_PropertyChainChanged(object sender, PropertyChainChangedEventArgs e)
    {
      ChangedChainProperties.Add(e);
    }
  }

  [Serializable]
  public class SimpleNotifyObject : NotifyObject
  {
    private string _FirePropertyWithLiteral;
    private string _FirePropertyWithEventArgs;
    private string _FirePropertyWithLambda;

    public SimpleNotifyObject()
    {
    }

    public string FirePropertyWithLiteral
    {
      get { return _FirePropertyWithLiteral; }
      set
      {
        if (_FirePropertyWithLiteral == value) return;
        _FirePropertyWithLiteral = value;
        OnPropertyChanged("FirePropertyWithLiteral");
      }
    }

    public string FirePropertyWithEventArgs
    {
      get { return _FirePropertyWithEventArgs; }
      set
      {
        if (_FirePropertyWithEventArgs == value) return;
        _FirePropertyWithEventArgs = value;
        OnPropertyChanged(new PropertyChangedEventArgs("FirePropertyWithEventArgs"));
      }
    }

    public string FirePropertyWithLambda
    {
      get { return _FirePropertyWithLambda; }
      set
      {
        if (_FirePropertyWithLambda == value) return;
        _FirePropertyWithLambda = value;
        OnPropertyChanged(() => FirePropertyWithLambda);
      }
    }
  }

  [Serializable]
  public class NotifyObjectWithSimpleStringDependencies : NotifyObject
  {
    private string _StringProperty1;
    private string _StringProperty2;

    public NotifyObjectWithSimpleStringDependencies()
    {
      RegisterDependent(() => StringDependentProperty, () => StringProperty1, () => StringProperty2);
    }

    public string StringProperty1
    {
      get { return _StringProperty1; }
      set
      {
        if (_StringProperty1 == value) return;
        _StringProperty1 = value;
        OnPropertyChanged(() => StringProperty1);
      }
    }

    public string StringProperty2
    {
      get { return _StringProperty2; }
      set
      {
        if (_StringProperty2 == value) return;
        _StringProperty2 = value;
        OnPropertyChanged(() => StringProperty2);
      }
    }

    public string StringDependentProperty
    {
      get { return string.Format("{0} {1}", StringProperty1, StringProperty2); }
    }
  }

  public class NotifyObjectWithSimpleMixedDependencies : NotifyObject
  {
    private int _IntProperty1;
    private DateTime _DateTimeProperty2;

    public NotifyObjectWithSimpleMixedDependencies()
    {
      RegisterDependent(() => StringDependentProperty, () => IntProperty1, () => DateTimeProperty2);
    }

    public int IntProperty1
    {
      get { return _IntProperty1; }
      set
      {
        if (_IntProperty1 == value) return;
        _IntProperty1 = value;
        OnPropertyChanged(() => IntProperty1);
      }
    }

    public DateTime DateTimeProperty2
    {
      get { return _DateTimeProperty2; }
      set
      {
        if (_DateTimeProperty2 == value) return;
        _DateTimeProperty2 = value;
        OnPropertyChanged(() => DateTimeProperty2);
      }
    }

    public string StringDependentProperty
    {
      get { return string.Format("{0} {1}", IntProperty1, DateTimeProperty2.ToString("yyyyMMdd")); }
    }
  }

  public class NotifyObjectWithDoubleDependencies : NotifyObject
  {
    private string _StringProperty1;
    private string _StringProperty2;

    public NotifyObjectWithDoubleDependencies()
    {
      RegisterDependent(() => StringDependentProperty1, () => StringProperty1, () => StringProperty2);
      RegisterDependent(() => StringDependentProperty2, () => StringDependentProperty1, () => StringProperty2);
    }

    public string StringProperty1
    {
      get { return _StringProperty1; }
      set
      {
        if (_StringProperty1 == value) return;
        _StringProperty1 = value;
        OnPropertyChanged(() => StringProperty1);
      }
    }

    public string StringProperty2
    {
      get { return _StringProperty2; }
      set
      {
        if (_StringProperty2 == value) return;
        _StringProperty2 = value;
        OnPropertyChanged(() => StringProperty2);
      }
    }

    public string StringDependentProperty1
    {
      get { return string.Format("{0} {1}", StringProperty1, StringProperty2); }
    }

    public string StringDependentProperty2
    {
      get { return string.Format("{0} - {1}", StringDependentProperty1, StringProperty2); }
    }
  }

  public class NotifyObjectWithCyclicDependencies : NotifyObject
  {
    private string _StringProperty1;
    private string _StringProperty2;
    private string _StringProperty3;

    public NotifyObjectWithCyclicDependencies()
    {
      RegisterDependent(() => StringProperty2, () => StringProperty1);
      RegisterDependent(() => StringProperty3, () => StringProperty2);
      RegisterDependent(() => StringProperty1, () => StringProperty3);
    }

    public string StringProperty1
    {
      get { return _StringProperty1; }
      set
      {
        if (_StringProperty1 == value) return;
        _StringProperty1 = value;
        OnPropertyChanged(() => StringProperty1);
      }
    }

    public string StringProperty2
    {
      get { return _StringProperty2; }
      set
      {
        if (_StringProperty2 == value) return;
        _StringProperty2 = value;
        OnPropertyChanged(() => StringProperty2);
      }
    }

    public string StringProperty3
    {
      get { return _StringProperty3; }
      set
      {
        if (_StringProperty3 == value) return;
        _StringProperty3 = value;
        OnPropertyChanged(() => StringProperty3);
      }
    }
  }

  public class NotifyObjectWithInnerFiring : NotifyObject
  {
    private string _StringProperty1;
    private string _StringProperty2;

    public NotifyObjectWithInnerFiring()
    {
      PropertyChanged += (s, e) =>
      {
        if (e.PropertyName == "StringProperty1")
          OnPropertyChanged(() => StringProperty2);
        if (e.PropertyName == "StringProperty2")
          OnPropertyChanged(() => StringProperty2);
      };
    }

    public string StringProperty1
    {
      get { return _StringProperty1; }
      set
      {
        if (_StringProperty1 == value) return;
        _StringProperty1 = value;
        OnPropertyChanged(() => StringProperty1);
      }
    }

    public string StringProperty2
    {
      get { return _StringProperty2; }
      set
      {
        if (_StringProperty2 == value) return;
        _StringProperty2 = value;
        OnPropertyChanged(() => StringProperty2);
      }
    }
  }
}
